package jmine.tec.persist.naturalkey;

import java.beans.PropertyDescriptor;
import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.UndeclaredThrowableException;

import org.apache.commons.beanutils.PropertyUtils;
import org.hibernate.Criteria;
import org.hibernate.criterion.MatchMode;
import org.hibernate.criterion.Order;
import org.hibernate.criterion.Restrictions;

import bancosys.tec.persist.Persistable;
import bancosys.tec.persist.exception.NaturalKeyNotDefinedException;
import bancosys.tec.persist.persister.annotation.NaturalKey;
import bancosys.tec.utils.reflection.ReflectionUtils;

/**
 * Contém as propriedades de natural key relativas a uma classe.
 * 
 * @author lundberg
 */
public class DefaultNaturalKeyHandler implements NaturalKeyHandler {

    private final String propertyName;

    private final NaturalKeyConverter<?> converter;

    private final Class<?> propertyType;

    private final boolean ignoreCase;

    private final PropertyDescriptor propertyDescriptor;

    /**
     * Constroi o bean contendo as propriedades da natural key.
     * 
     * @param clazz classe da entidade persistível
     * @param registry registro de natural key
     */
    public DefaultNaturalKeyHandler(Class<? extends Persistable> clazz, NaturalKeyHandlerFactory registry) {
        this.propertyDescriptor = findNaturalKeyProperty(clazz);
        this.propertyName = this.propertyDescriptor.getName();
        this.propertyType = this.propertyDescriptor.getPropertyType();
        this.ignoreCase = PropertyUtils.getReadMethod(this.propertyDescriptor).getAnnotation(NaturalKey.class).ignoreCase();
        this.converter = registry.getConverter(this.propertyType);
    }

    /**
     * Encontra a propriedade anotada com NaturalKey
     * 
     * @param clazz clazz
     * @return PropertyDescriptor
     */
    private static PropertyDescriptor findNaturalKeyProperty(Class<? extends Persistable> clazz) {
        // Procura pela anotação @NaturalKey
        PropertyDescriptor[] properties = PropertyUtils.getPropertyDescriptors(clazz);
        for (final PropertyDescriptor property : properties) {
            Method getter = PropertyUtils.getReadMethod(property);
            if (getter != null) {
                // quando encontrar a anotação guarda o nome do atributo e interrompe o loop
                if (getter.isAnnotationPresent(NaturalKey.class)) {
                    return property;
                }
            }
        }
        throw new NaturalKeyNotDefinedException(clazz);
    }

    /**
     * Adiciona uma restrição de igualdade com a natural key passada. A restrição pode ser feita com ignoreCase caso assim esteja anotada a
     * entidade.
     * 
     * @param criteria criteria que deve ser configurado
     * @param naturalKey valor procurado da natural key
     * @throws InvalidNaturalKeyException caso não seja possível converter a natural key para o tipo correto.
     */
    public void addRestrictionEq(Criteria criteria, Serializable naturalKey) throws InvalidNaturalKeyException {
        if (this.ignoreCase) {
            criteria.add(Restrictions.ilike(this.propertyName, this.converter.convert(naturalKey).toString(), MatchMode.EXACT));
        } else {
            criteria.add(Restrictions.eq(this.propertyName, this.converter.convert(naturalKey)));
        }
    }

    /**
     * Adiciona uma restrição do tipo "like", sempre case insensitive.
     * 
     * @param criteria criteria que deve ser configurado
     * @param naturalKey valor sendo procurado na natural key
     * @param matchMode tipo de match que deve ser utilizado
     * @throws InvalidNaturalKeyException caso não seja possível converter a natural key para o tipo correto.
     */
    public void addRestrictionLike(Criteria criteria, Serializable naturalKey, MatchMode matchMode) throws InvalidNaturalKeyException {
        criteria.add(Restrictions.ilike(this.propertyName, this.converter.convert(naturalKey).toString(), matchMode));
    }

    /**
     * Adds to the criteria an asc order to the natural key
     * 
     * @param criteria criteria
     */
    public void addOrderAsc(Criteria criteria) {
        criteria.addOrder(Order.asc(this.propertyName));
    }

    /**
     * {@inheritDoc}
     */
    public Serializable getNaturalKey(Persistable persistable) {
        try {
            return (Serializable) this.propertyDescriptor.getReadMethod().invoke(persistable);
        } catch (IllegalAccessException e) {
            throw new RuntimeException("Cannot read natural key from type: " + this.propertyDescriptor.getReadMethod().getDeclaringClass(),
                    e);
        } catch (InvocationTargetException e) {
            ReflectionUtils.rethrowErrorOrRuntime(e);
            throw new UndeclaredThrowableException(e.getTargetException());
        }
    }

    /**
     * {@inheritDoc}
     */
    public Class<? extends Persistable> getTargetClass() {
        return Persistable.class;
    }
}
